-
Notifications
You must be signed in to change notification settings - Fork 5
Refactor algorithm selection logic #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Codecov ReportAttention: Patch coverage is
🚀 New features to boost your workflow:
|
|
What this PR calls EDIT: So to clarify, the proposal would be to change |
lkdvos
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a reasonable change to me, should definitely offer quite a bit of flexibility.
If we decide on this, can you also look at exporting default_algorithm, since that would be the main point to overload, and check if the docs need updating for this new approach?
What do you think of making it public but not exporting it for now, indicating it is more of a syntax for overloading rather than using externally? (EDIT: I made |
|
Currently, if you pass an algorithm type or Symbol as the |
|
I don't have any strong feelings about public or exported in this case. Given that our use-cases are mostly to use this as a low-level library to build on, I don't think it matters too much. As you noticed I'm a bit more worried about the type stability though. |
There are long debates about that: https://discourse.julialang.org/t/is-compat-jl-worth-it-for-the-public-keyword/119041, the approach I use here is the one recommended in the Julia docs: https://docs.julialang.org/en/v1.12-dev/manual/modules/#Export-lists. Some people are weary of Compat.jl because it causes a lot of invalidations and uses type piracy, so if there is a simple way to avoid it it seems better to do so, but I'm fine either way.
I doubt it was type stable before but I can double check. I also tried |
|
I think basically what we are doing is analogous to this minimal example: julia> f(; x) = Val{x}()
f (generic function with 1 method)
julia> @code_warntype f(; x=:x)
MethodInstance for Core.kwcall(::@NamedTuple{x::Symbol}, ::typeof(f))
from kwcall(::NamedTuple, ::typeof(f)) @ Main REPL[56]:1
Arguments
_::Core.Const(Core.kwcall)
@_2::@NamedTuple{x::Symbol}
@_3::Core.Const(Main.f)
Locals
@_4::Symbol
x::Symbol
Body::Val
1 ─ Core.NewvarNode(:(@_4))
│ %2 = Core.isdefined(@_2, :x)::Core.Const(true)
└── goto #3 if not %2
2 ─ (@_4 = Core.getfield(@_2, :x))
└── goto #4
3 ─ Core.Const(:(Core.UndefKeywordError(:x)))
└── Core.Const(:(@_4 = Core.throw(%6)))
4 ┄ %8 = @_4::Symbol
│ (x = %8)
│ %10 = Base.keys(@_2)::Core.Const((:x,))
│ %11 = (:x,)::Core.Const((:x,))
│ %12 = Base.diff_names(%10, %11)::Core.Const(())
│ %13 = Base.isempty(%12)::Core.Const(true)
└── goto #6 if not %13
5 ─ goto #7
6 ─ Core.Const(:(Base.kwerr(@_2, @_3)))
7 ┄ %17 = Main.:(var"#f#1")::Core.Const(Main.var"#f#1")
│ %18 = x::Symbol
│ %19 = (%17)(%18, @_3)::Val
└── return %19
julia> @code_warntype (() -> f(; x=:x))()
MethodInstance for (::var"#5#6")()
from (::var"#5#6")() @ Main REPL[61]:1
Arguments
#self#::Core.Const(var"#5#6"())
Body::Val{:x}
1 ─ %1 = (:x,)::Core.Const((:x,))
│ %2 = Core.apply_type(Core.NamedTuple, %1)::Core.Const(NamedTuple{(:x,)})
│ %3 = (:x,)::Core.Const((:x,))
│ %4 = (%2)(%3)::Core.Const((x = :x,))
│ %5 = Core.kwcall(%4, Main.f)::Core.Const(Val{:x}())
└── return %5
julia> using TestExtras
julia> @constinferred f(; x=:x)
Val{:x}()
julia> @constinferred f(; x=Symbol("x"))
Test Failed at REPL[75]:1
Expression: @constinferred f(; x = Symbol("x"))
Evaluated: Val{:x} != Val
ERROR: There was an error during testingso it seems like at least in principle it should be inferrable. |
src/algorithms.jl
Outdated
| function select_algorithm end | ||
|
|
||
| function _select_algorithm(f, A::AbstractMatrix, alg::AbstractAlgorithm) | ||
| function select_algorithm(f, A; alg=nothing, kwargs...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure using multiple dispatch via _select_algorithm to deal with the different cases of alg keywords is the right approach, as it might put more strain on the compiler. I don't think there is need to be able to extend this functionality, so something like
function select_algorithm(f, A; alg=nothing, kwargs...)
if isnothing(alg)
return default_algorithm(f, A; kwargs...)
elseif alg isa AbstractAlgorithm
isempty(kwargs) ||
throw(ArgumentError("Additional keyword arguments are not allowed when an algorithm is specified."))
return alg
elseif
...
end
endshould work. Or does the _select_algorithm lowering actually help with the lowering?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm totally fine with that, using dispatch was a bit arbitrary. I generally like using dispatch rather than if-statements purely as a style thing, but if you and the compiler prefer if-statements I can switch to that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying this out locally, an unfortunate side affect of this is that functions like left_orth/right_orth become type unstable, since before they were calling _select_algorithm which is type stable and select_algorithm isn't with the new design (I think what happens is that certain branches of select_algorithm aren't type stable, so then the entire thing becomes type unstable even for cases that used to be type stable).
I'll investigate that more. But maybe it would be better to just change select_algorithm to accept alg as a positional argument and go back to using dispatch. That should solve all type stability problems being discussed above as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the _select_algorithm lowering is better for type stability, I am certainly fine with that, and there is no need to waste time trying to trick the compiler to make the if elseif else construction inferable as well.
test/eig.jl
Outdated
| for alg in (LAPACK_Simple(), LAPACK_Expert()) | ||
| for alg in | ||
| (LAPACK_Simple(), LAPACK_Expert(), LAPACK_Simple, LAPACK_Expert, :LAPACK_Simple, | ||
| :LAPACK_Expert) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the idea that if we only modify this list for the eig_full tests, this will test the selection/default functionality and should thus also work for the other factorisations? I am fine with this, and definitely think we should not run all tests 3 times over as this would be a large amount of unnecessary compute. Just checking if this is the plan?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now I just added it here to get some more feedback on the design before going through the work of adding it to all of the factorization tests, and also as you say I also wanted to hear your opinion on whether we even want to test all of the factorizations.
Maybe I could add it to one more factorization test, and also add some standalone tests of select_algorithm and default_algorithm.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Standalone select_algorithm and default_algorithm tests are definitely a good idea. Regarding the names: I like those names 😄 !
Co-authored-by: Jutho <[email protected]>
|
Regarding the type stability, I just remembered/realized that this change has an additional side-effect that might affect this: before we didn't have two layers ( |
|
Ok, I've solved most type instability issues and added more tests. I had to rely both on forcing method specialization and Oddly, the one case that still isn't type stable is specifying |
|
This is ready for review. All type stability issues are fixed, except for one issue in Julia 1.10, I would vote for letting that slide (as opposed to making the code more complicated to fix it) but please let me know if you want me to look into that. In the latest, I was able to avoid all uses of |
| if VERSION < v"1.11" | ||
| # This is type unstable on older versions of Julia. | ||
| U, S, Vᴴ = svd_compact(A; alg) | ||
| else | ||
| U, S, Vᴴ = @constinferred svd_compact(A; alg=($alg)) | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the case I mentioned that isn't type stable in older versions of Julia. I'd vote for letting this slide and telling users to use a newer versions of Julia if they care about type stability, let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm assuming that this is also just some strange interplay with the non-heterogeneous tuple of algs, so this is more of a test artifact than a real problem. Definitely okay with letting this slide.
lkdvos
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only looked over this briefly on my phone so it would be good if @Jutho can also give his blessing, but this looks like a nice implementation now, I'm very happy the constprop annotations weren't necessary in the end.
Jutho
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice PR, only pending comment is a suggestion in an attempt to further improve the clarity of the select_algorithm doc string.
Co-authored-by: Jutho <[email protected]>
Jutho
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks ready to be merged! Feel free to merge yourself @mtfishman , otherwise without reaction I will merge this tonight.
|
Thanks again for the careful review. Should we register v0.2 after #25 gets merged? |
|
Yes that sounds good to me. I went through #25 and do not have any comments. |
Co-authored-by: Jutho <[email protected]> Co-authored-by: Lukas Devos <[email protected]>
This refactors the algorithm selection logic (
select_algorithm) in terms of two layers. In this new design, there is a generic functionselect_algorithmfor any factorization function and matrix type that processes thealgkeyword argument, and either uses the algorithm that is specified or if none is specified forwards to a lower level generic functiondefault_algorithm, which then chooses a default algorithm depending on the factorization function and matrix type.The motivation for this was that I started making a PR to make
select_algorithmstricter about how it handles keyword arguments, but it was cumbersome to do since theselect_algorithmlogic for processing keyword arguments was repeated for each factorization function. In addition, I added logic for allowing an algorithm type (in addition to a Symbol and algorithm object) to be passed asalg, which was also easier to implement in the new design.@lkdvos and I were discussing the naming of these two layers of functions (currently
select_algorithmanddefault_algorithm), he proposedselect_algorithm->_select_algorithmanddefault_algorithm->select_algorithm. I'm definitely open to suggestions, curious what you think @Jutho.To-do:
default_algorithm.select_algorithm/default_algorithm.